/*
 * @描述: 
 * @创建者: kilomChou
 * @Date: 2021-05-01 15:06:47
 * @最后编辑: kilomChou
 * @LastEditTime: 2021-10-19 16:57:54
 */

import fs from "fs";
import path from "path";
import xlsx from "node-xlsx"
import { BinaryWriter, ObjEncoder, ValueType, ValueType as VT } from "k-coder";

/** Typescript 表vo对象构建 */
export default class TsXlsxVoBuilder {

    private vos: IXlsxVo[] = []
    private maps: IMap[] = []

    constructor(
        /** 表格目录 */
        readonly inputDir: string,
        /** 代码输出目录 */
        readonly codeOutputDir: string,
        /** 数据输出目录 */
        readonly dataOutputDir: string,
        /** 导出类型 空为全导 */
        readonly exportTy?: string,
        /** 模式 */
        readonly mode: string = "",
        /** VO前缀 */
        readonly prefix: string = "X",
        /** 输出类名 */
        readonly clsName = "XlsxVo",
    ) {

    }

    /** 构建表格对象  */
    build() {
        this.loadAllXlsx();
        this.codeOutputDir && this.buildCode();
        this.dataOutputDir && this.buildData();
    }

    /** 读取所有表格 */
    private loadAllXlsx() {
        //遍历表格目录及其子目录读取表格
        (function eachDir(dir: string, fn: (filePath: string, fileName: string) => void) {
            fs.readdirSync(dir).forEach(fileName => {
                let filePath = path.resolve(dir, fileName);
                if (fs.statSync(path.resolve(dir, fileName)).isDirectory()) {
                    //递归遍历子目录
                    eachDir(filePath, fn);
                } else {
                    fn(filePath, fileName);
                }
            })
        })(this.inputDir, (filePath, fileName) => {
            let match = fileName.match(/^([^~].*)\.xlsx$/)
            if (match) {
                //检查命名是否规范
                console.log(filePath);
                assert(!match[1].match(/[^_0-9a-zA-Z]/), `[${fileName}]命名不规范`);
                let file = fs.readFileSync(filePath);
                this.loadXlsx(file, match[1], filePath);
            }
        })
        //根据表名排序下
        this.vos.sort((a, b) => (a.name < b.name) ? -1 : a.name > b.name ? 1 : 0);
    }

    /** 读取单个表格 */
    private loadXlsx(file: Buffer, fileName: string, filePath: string) {
        let table = xlsx.parse(file);
        assert(table[0], `${fileName}`);
        let data: any[][] = table[0].data;
        let path = filePath.replace(this.inputDir, "").replace(/\\/g, "/").replace("/", "");
        let vo: IXlsxVo = {
            path,
            name: fileName,
            isCfg: data[0][0] == "#cfg",
            fields: [],
            data: [],
        }
        let maps = this.maps;
        if (vo.isCfg) {
            let temp: any[][] = [[], [], [], [], []];
            let i = 0;
            while (data[++i] && data[i][2]) {
                //注释
                temp[0].push(data[i][3]);
                //字段
                temp[1].push(data[i][2]);
                //类型
                temp[2].push(data[i][1]);
                //导出类型
                temp[3].push(data[i][0]);
                //值
                temp[4].push(data[i][4]);
            }
            data = temp;
        }
        for (let i = 4, len = data.length; i < len; i++) {
            //过滤掉没id的,有可能是空行
            data[i][0] && vo.data.push(data[i]);
        }
        //0-注释 1-字段 2-类型 3-导出类型
        let [notes, fields, types, exportTys] = data;
        //根据导出类型，筛选出需要导出的字段
        exportTys.forEach((exportTy, idx) => {
            if (!this.exportTy || this.exportTy.split(",").includes(exportTy)) {
                let note = notes[idx];
                let field: string = fields[idx];
                let type: string = types[idx];
                if (field == "#") return;
                if (field.match(/#(map|enum)/)) {
                    parseMap(field, note, type);
                } else {
                    parseField(idx, note, field, type);
                }
            }
        })

        /** 解析字段 */
        function parseField(col: number, note: string, name: string, type: string) {
            assert(!name.match(/[^_0-9a-zA-Z]/), `[${fileName}.xlsx] >>> 字段<${name}>命名不规范`);
            let match = type.match(/([_0-9a-zA-Z]+)(?:\((\d+)\))*(?:\[(\d*)\])*/);
            assert(match, `[${fileName}.xlsx] >>> ${note}：类型<${type}>解析错误`);
            let floatParam = match[2] ? parseInt(match[2]) : 0;
            let arrVec = match[3] === "" ? 1 : match[3] ? parseInt(match[3]) : 0;
            let vt = getVt(match[1], arrVec);
            type = getTypeCode(vt, arrVec);
            vo.fields.push({
                col,
                note,
                name,
                type,
                vt,
                floatParam,
                arrVec
            })
        }

        /** 解析映射 */
        function parseMap(mapType: string, note: string, type: string) {
            let match = type.match(/([_0-9a-zA-Z]+),([_0-9a-zA-Z]+)+,([_0-9a-zA-Z]+)+,([_0-9a-zA-Z]+)+/);
            assert(match, `[${fileName}.xlsx] >>> ${note}：${mapType}<${type}>解析错误`);
            let name = match[1];
            let key = match[2];
            let val = match[3];
            let fieldNote = match[4];
            let map: IMap = {
                mapType,
                path,
                note,
                name,
                data: vo.data,
                noteCol: -1,
                keyCol: -1,
                valCol: -1,
            }
            vo.fields.forEach(field => {
                switch (field.name) {
                    case key:
                        map.keyCol = field.col;
                        break;
                    case val:
                        map.valCol = field.col;
                        break;
                    case fieldNote:
                        map.noteCol = field.col;
                        break;
                }
            })
            assert(map.keyCol != -1, `[${fileName}.xlsx] >>> ${note}：${mapType}<${key}>字段未找到`);
            assert(map.valCol != -1, `[${fileName}.xlsx] >>> ${note}：${mapType}<${val}>字段未找到`);
            assert(map.noteCol != -1, `[${fileName}.xlsx] >>> ${note}：${mapType}<${fieldNote}>字段未找到`);
            maps.push(map);
        }

        /** 获取值类型 */
        function getVt(type: string, arrVec: number) {
            switch (type) {
                case "bool":
                    return arrVec ? VT.boolArr : VT.bool;
                case "uint":
                    return arrVec ? VT.uintArr : VT.uint;
                case "int":
                    return arrVec ? VT.intArr : VT.int;
                case "float":
                    return arrVec ? VT.floatArr : VT.float;
                case "floatUint":
                    return arrVec ? VT.floatUintArr : VT.floatUint;
                case "floatInt":
                    return arrVec ? VT.floatIntArr : VT.floatInt;
                case "floatStr":
                    return arrVec ? VT.floatStrArr : VT.floatStr;
                case "str":
                    return arrVec ? VT.strArr : VT.str;
                case "obj":
                    return VT.obj;
                case "arr":
                    return VT.arr;
            }
        }

        /** 获取值类型代码 */
        function getTypeCode(vt: ValueType, arrVec: number) {
            let type = ""
            switch (vt) {
                case VT.bool:
                case VT.boolArr:
                    type = "boolean";
                    break;
                case VT.uint:
                case VT.int:
                case VT.float:
                case VT.floatUint:
                case VT.floatInt:
                case VT.floatStr:
                case VT.uintArr:
                case VT.intArr:
                case VT.floatArr:
                case VT.floatUintArr:
                case VT.floatIntArr:
                case VT.floatStrArr:
                    type = "number";
                    break;
                case VT.str:
                case VT.strArr:
                    type = "string";
                    break;
                case VT.obj:
                    type = "any";
                    break;
                case VT.arr:
                    type = "any[]";
                    break;
            }
            while (arrVec--) {
                type += "[]";
            }
            return type;
        }
        this.vos.push(vo);
    }

    private buildCode() {
        let code = "//工具生成，请不要手动修改...\n";

        //写入kcoder初始化
        code += "let BinaryReader: any, ObjDecoder: any\n";
        code += "/** 传入k-coder模块来进行初始化 */\n";
        code += `export function init${this.clsName}(kcoder: { BinaryReader: any, ObjDecoder: any }) {\n`;
        code += "    BinaryReader = kcoder.BinaryReader\n";
        code += "    ObjDecoder = kcoder.ObjDecoder\n";
        code += "}\n\n";

        //写入读取表数据方法
        code += "let dataDic = {}\n"
        code += "let loadedDic = {}\n"
        code += "/** 读取表数据 */\n"
        code += "export function loadData(bytes: Uint8Array) {\n"
        code += "    let r = BinaryReader.alloc(bytes)\n"
        code += "    while (r.hasNext) dataDic[r.str] = r.cut(r.uint)\n"
        code += "    for (let name in loadedDic) { loadedDic[name].load() }\n"
        code += "}\n\n"

        //写入读取结构方法
        code += "function loadStruct(name: string) {\n";
        code += "    let r = BinaryReader.alloc(dataDic[name])\n";
        code += "    let struct = []\n";
        code += "    //[读取] >>> uint字段数量\n";
        code += "    let len = r.uint\n";
        code += "    while (len--) {\n";
        code += "        //[读取] >>> str字段 uint类型\n";
        code += "        let str = r.str, vt = r.uint, st = [str, vt]\n";
        code += `        if (vt > ${ValueType.arr}) {\n`;
        code += "            //[读取] >>> uint数组维度\n";
        code += "            st.push(r.uint)\n";
        code += "        }\n";
        code += `        if ((vt > ${ValueType.float} && vt < ${ValueType.floatStr}) || (vt > ${ValueType.floatArr}) && (vt < ${ValueType.floatStrArr})) {\n`;
        code += "            //[读取] >>> uint float特殊参数\n";
        code += "            st.push(r.uint)\n";
        code += "        }\n";
        code += "        struct.push(st)\n";
        code += "    }\n";
        code += "    return { r, struct }\n";
        code += "}\n\n";

        //写入cfg类load方法
        code += "class Cfg<Vo> {\n";
        code += "    constructor(private readonly name: string) { }\n";
        code += "    /** vo实例 */\n";
        code += "    get vo(): Vo { this.load(); return this.vo }\n";
        code += "    private load() {\n";
        code += "        let { r, struct } = loadStruct(this.name)\n";
        code += `        Object.defineProperty(this, "vo", { value: ObjDecoder.deStruct(r, struct) })\n`;
        code += "        r.recy()\n";
        code += "        loadedDic[this.name] = this\n";
        code += "    }\n";
        code += "}\n\n";

        //写入vo类load方法
        code += "class Xlsx<Vo> {\n";
        code += "    constructor(private readonly name: string) { }\n";
        code += "    /** 通过id获取 */\n";
        code += "    get(id: number | string): Vo { this.load(); return this.get(id) }\n";
        code += "    /** 通过idx获取 */\n";
        code += "    getAt(idx: number): Vo { this.load(); return this.getAt(idx) }\n";
        code += "    /** 遍历所有vo */\n";
        code += "    each(fn: (data: Vo, idx: number) => void) { this.load(); this.each(fn) }\n";
        code += "    /** vo数量 */\n";
        code += "    get count(): number { this.load(); return this.count }\n";
        code += "    private load() {\n";
        code += "        let { r, struct } = loadStruct(this.name)\n";
        code += "        let vos: any[] = [], id2vo: any = {}\n";
        code += "        while (r.hasNext) {\n";
        code += "            let vo = ObjDecoder.deStruct(r, struct)\n";
        code += `            vos.push(id2vo[vo.id] = vo)\n`;
        code += "        }\n";
        code += "        r.recy()\n";
        code += "        this.get = id => id2vo[id]\n";
        code += "        this.getAt = idx => vos[idx]\n";
        code += "        this.each = callback => vos.forEach(callback)\n";
        code += `        Object.defineProperty(this, "count", { value: vos.length })\n`;
        code += "        loadedDic[this.name] = this\n";
        code += "    }\n";
        code += "}";

        //写入vo代码
        this.vos.forEach(vo => {
            let constName = this.prefix + toFirstUpperCase(vo.name);
            let voName = constName + "Vo";
            code += `\n\n/** [${vo.path}] */\n`;
            code += `export const ${constName} = new ${vo.isCfg ? "Cfg" : "Xlsx"}<${voName}>("${vo.name}")\n`;
            code += `export interface ${voName} {\n`;
            vo.fields.forEach(field => {
                let { note, name, type } = field;
                if (note) code += `    /** ${note} */\n`;
                code += `    readonly ${name}: ${type}\n`;
            })
            code += `}`;
        });

        //写入映射代码
        this.maps.forEach(map => {
            if (map.mapType == "#map") {
                code += `\n\nconst _${map.name} = {\n`;
                map.data.forEach(row => {
                    let val = row[map.valCol];
                    code += `    /** ${row[map.noteCol]} */\n`;
                    code += `    ${row[map.keyCol]}: ${parseInt(val) != NaN ? `${val}` : `"${val}"`},\n`;
                })
                code += `}\n`;
                code += `/** ${map.note}[${map.path}] */\n`;
                code += `export const ${map.name}:Readonly<typeof _${map.name}> = _${map.name}`;
            } else {
                code += `\n\n/** ${map.note}[${map.path}] */\n`;
                code += `export ${this.mode == "not-const" ? "" : "const "}enum ${map.name} {\n`;
                map.data.forEach(row => {
                    let val = row[map.valCol];
                    code += `    /** ${row[map.noteCol]} */\n`;
                    code += `    ${row[map.keyCol]} = ${parseInt(val) != NaN ? `${val}` : `"${val}"`},\n`;
                })
                code += `}`;
            }
        });

        fs.writeFileSync(path.resolve(this.codeOutputDir, this.clsName + ".ts"), code, { encoding: "utf-8", flag: "w+" });
    }

    private buildData() {
        let writer = BinaryWriter.alloc();
        this.vos.forEach(vo => {
            let w = BinaryWriter.alloc();
            let { name, fields, data } = vo;

            //[写入] >>> str表名
            w.str = name;

            let dataW = BinaryWriter.alloc();
            //[写入] >>> uint字段数量
            dataW.uint = fields.length;

            let struct = [];
            fields.forEach(field => {
                let { vt, floatParam, arrVec } = field;
                //[写入] >>> str字段名，uint值类型
                let st = [field.name, vt];
                dataW.str = field.name;
                dataW.uint = vt;
                if (arrVec) {
                    ///[写入] >>> uint数据维度
                    dataW.uint = arrVec;
                    st.push(arrVec);
                }
                if (floatParam) {
                    //[写入] >>> uint float特殊参数
                    dataW.uint = floatParam;
                    st.push(floatParam);
                }
                struct.push(st);
            })

            data.forEach(dataRow => {
                let obj: any = {};
                fields.forEach((field, idx) => {
                    let val = dataRow[field.col];
                    if (val != undefined) {
                        obj[field.name] = struct[idx][1] >= VT.obj ? JSON.parse(val) : val;
                    }
                });
                ObjEncoder.enStruct(dataW, obj, struct);
            })


            //[写入] >>> uint数据长度 数据
            w.uint = dataW.len;
            w.concat(dataW.recy());
            //并入总writer
            writer.concat(w.recy());
        })
        //输出data文件
        fs.writeFileSync(path.resolve(this.dataOutputDir, "data.bin"), writer.end(), { flag: "w+" });
    }
}

/** 首字母大写 */
function toFirstUpperCase(str: string) {
    return str[0].toUpperCase() + str.slice(1);
}

/** 断言失败退出程序并输出错误信息 */
function assert(cond: any, msg?: string) {
    if (!cond) {
        console.log(msg);
        process.exit();
    }
    return cond;
}

interface IField {
    col: number,
    note: string,
    name: string,
    type: string,
    vt: VT,
    floatParam?: number
    arrVec?: number
}

interface IXlsxVo {
    path: string,
    name: string,
    isCfg: boolean,
    fields: IField[],
    data: any[]
}

interface IMap {
    mapType: string,
    path: string,
    note: string,
    name: string,
    noteCol: number,
    keyCol: number,
    valCol: number
    data: any[],
}